Seminario de Lenguajes

Opción C

La cátedra

Profesores

Jefes de Trabajos Prácticos

  • Andrés Barbieri
  • Nahuel Cuesta Luengo

Ayudantes

  • Carina Girón
  • Matías Ferrigno
  • Emilia Corrons
  • Valeria Soria

Colaboradores

  • Lucas Di Cunzolo
  • Joaquín Gamba

Contacto

Horarios

Horarios de Teoría

  • Martes de 9:30hs a 11:00hs Aula 1-1

Horarios de Práctica

  • Martes de 8:00hs a 9:30hs Aula 1-1
  • Viernes de 11:30hs a 13:30hs Aula 1-1

Bibliografía

  • El Lenguaje C. Kernighan & Ritchie
  • C con ejemplos. Perry, Greg
  • Efficient C programming. Weiss, Mark Allen
  • C in a nutshell. Prinz, Peter

    http://catalogo.info.unlp.edu.ar

Modo de aprobación

La aprobación de la cursada estará dada por la aprobación de cuatro evaluaciones en máquina presenciales:

  • Si se aprueban las cuatro evaluaciones no se rinde parcial integrador
  • Cada evaluación desaprobada se rendirá como un tema en el parcial integrador
  • Puede no aprobarse ninguna evaluación, siendo necesario rendir el parcial integrador completo
  • TP integrador individual o máximo de 2 personas, defendible en un coloquio individual

Aprobación de la materia

  • La aprobación de la materia estará dada por la aprobación de la Cursada
  • Con ella se obtiene un 6 (seis)

Si se desea mejorar la nota se podrá realizar una extensión del trabajo realizado en la cursada. La nota final se promediará con el desempeño durante la cursada

Clase 1

Temario

  • Historia de C
  • Características generales
  • Compilación de un programa
  • Tipos de datos
  • Variables
  • Operadores

Historia de C

Un poco de historia

  • Desarrollador entre 1969/1973 por Dennis Ritchie
  • A fines de 1974 fue utilizado para reimplementar el sistema operativo Unix
  • En 1980 ANSI comenzó la estandarización del lenguaje
  • A partir de 1990, ISO adopta ANSI C en el estándar conocido como C89/C90
  • A fines de los '90s, se revisa el estándar y surge C99 que incorpora nuevas características que fueron revolucionarias

Quiénes lo utilizan

  • Nuevos lenguajes mantienen expresiones similares en su sintaxis, como por ejemplo: C++, PERL, Javascript, Java, PHP, entre otros.
  • En el desarrollo de sistemas operativos, bases de datos, juegos, compiladores, nuevos lenguajes, etc.

Además, programar en C, motiva la adopción de buenas prácticas de programación

Características

  • Combina elementos de alto nivel con características de bajo nivel
  • Es un lenguaje pequeño, con una sintaxis simple
  • Es orientado a expresiones
  • Es base de lenguajes modernos
  • Código "portable"
  • Extensible mediante el uso de librerías
  • Contempla sólo tipos de datos básicos

Características

  • No es orientado a objetos
  • No es un lenguaje de bajo nivel
  • No provee primitivas de entrada salida
  • No realiza comprobación de errores en tiempo de ejecución

Proceso de compilación

Hola mundo!

Asumiendo el archivo se llama hola_mundo.c

#include <stdio.h>

int main() 
{
    printf("Hola Mundo\n");
    return (0);
}
$ gcc hola_mundo.c -o hola_mundo -Wall

Descargar ejemplo

Fases en la creación de un programa

Fases de compilación

Tipos de datos

Tipos de datos

  • Definen la representación interna de una variable y un conjunto de valores posibles.
  • La definición original de C era sumamente permisiva respecto de las conversiones de tipos
    • Actualmente se requiere una adecuada declaración y conversión explícita
  • Tipos de datos en C:
    • Básicos: caracteres, enteros y números de punto flotante, punteros.
    • Derivados: arreglos, estructuras y uniones.

Tipos de datos básicos

Tipo Descripción 32bits 64bits
char caracter 8 8
short entero corto con signo 16 16
int entero con signo 32 32
long entero largo con signo 32 64

Tipos de datos básicos

Tipo Descripción 32bits 64bits
float flotante simple 32 32
double flotante doble 64 64
pointer puntero a una posición de memoria 32 64
void idem char. Empleado para tipos genéricos - -

No existe el tipo boolean

Constantes literales: Punto Flotante

La expresión:

3.45 – 1.2e+34

Significa 3.45 - (1.2*1034)

Constantes literales: Enteros

Base Representación
Decimal 14
Hexadecimal 0xE
Octal 016

Constantes literales: Enteros

Calificadores o modificadores

Determinan si el entero es con signo, corto o largo:

Literal Representación Significado
23L decimal 23 long int
23LU decimal 23 unsigned long int
023LU octal 19 unsigned long int
0XFUL hexadecimal 15 unsigned long int

Descargar ejemplo

Constantes literales: Char

  • Permiten representar letras mayúsculas, minúsculas, números de 0 a 9, signos de puntuación, no imprimibles, etc.
  • Se manejan como char o int (valor ASCII) de forma indistinta

Constantes literales: Char

Las comillas

  • Comilla simple: usada para un char
  • Comilla doble: usada para un string

Constantes literales: Char

Ejemplos

char Significado
'A' Letra A mayúscula
'\n' Salto de linea. Equivale a '\x0A'
'\0' Fin de string o NULL o cero
'\x0A' Idem '\n' en hexadecimal
'\012' Idem '\n' en octal

Constantes literales: Char

Los strings

  • Los strings se representan con comillas dobles.
  • Por ello, 'A' no es lo mismo que "A"
  • El string "A" es un arreglo con los siguientes elementos: ['A','\0']

Constantes enumerativas: Enum

Se utilizan para enumerar una lista de valores constantes

enum estado_civil {casado, soltero, viudo, separado, divorciado, NSNC,
estado_civil_TOPE };

enum meses { ENE=1, FEB, MAR, ABR, MAY, JUN, JUL, AGO, SEP, OCT, NOV, DIC,
meses_TOPE };

Descargar ejemplo

Punteros

  • Representan una variable que apunta a otra
  • Contienen la dirección de memoria de otra variable
  • Un puntero consta de dos partes:
    • La dirección apuntada
    • El contenido apuntado.

Conceptos del manejo de memoria

  • Cada posición de memoria:
    • Almacena un byte
    • Es accesible mediante una dirección específica
  • Las direcciones de memoria son consecutivas
  • Dependiendo del tipo de memoria al que apunte un puntero, la memoria accederá a todos los bytes que representen ese tipo.
  • Los programas comparten el espacio de direcciones con el sistema operativo
  • Los datos del programa y el programa se almacenan en memoria al ser ejecutados

Distribución de la memoria

  • La memoria de un programa en ejecución se organiza en segmentos
    • Text segment: código compilado del programa
    • Stack segment: almacena variables automáticas declaradas en funciones
    • Heap segment: almacena datos dinámicos

Fases de compilación

Distribución de la memoria

int x;        /* Variable global */

int main() {

  int y;      /* Variable automatica */

  char *str;  /* Variable automatica cuyo valor es la posición
               * de memoria de otra variable
               */

  str = malloc(50); /* Es en esta instancia donde se aloca memoria
                     * en la heap 
                     */
  ...
}

Esquema de memoria

int main( ) {

  int v_entero;
  int *p;
  v_entero = 7;
  p = &v_entero;
  ...

Esquema de memoria: gráfico

Variables

Variables

  • Representa una posición de memoria que se le asigna un nombre y posee un cierto tipo de datos
  • Se usan en expresiones para realizar cálculos
  • Deben ser declaradas antes de ser utilizadas.
  • El operador & permite acceder a la dirección de la variable
  • Son sensibles a mayúsculas y minúsculas

Variables: inicialización

Es posible realizar la incialización en la declaración

int x = 9;
int j, x = 12;
int j = 12, x;
char c = 'x';
const int x = 12;
char c, linea[80];
char nombre[] = "Juan";
const char mensaje[] = "Saludos!!";

Recordar que las últimas dos cadenas son arreglos de char terminadas en '\0'


['J','u','a','n','\0']
['S', 'a', 'l', 'u', 'd', 'o', 's', '!', '!', '\0']

Conversiones de tipos

  • Es posible mezclar tipos de datos en una expresión:
    • Conversiones explícitas: (tipo) expresion
    • Conversiones implícitas: lvalue = rvalue

Conversiones implícitas

  • De int a char se eliminan los bits de orden superior
  • De float a int se trunca la parte fraccionaria
  • De double a float se aplica redondeo
  • Los enteros de mayor magnitud (como long) se convierten en short o en char eliminando los bits de orden superior

Descargar ejemplo

Conversiones implícitas: Reglas

  • Si cualquier operando es long double, se convierte el otro a long double
  • De otra manera, si cualquier operando es double, se convierte el otro a double
  • De otra manera, si cualquier operando es float, se convierte el otro a float
  • De otra manera, si cualquier operando es long, se convierte el otro a long
  • De otra manera, si cualquier operando es int, se convierte el otro a int
  • Finalmente, se convierte char a short

Conversiones implícitas: Ejemplo

Asumiendo el siguiente código

  int x = 64, y = 357;
  float f = 4.95;
  char c;
  c = x;
  x = c;
  c = y;
  y = c;
  y = f;

¿Con qué valores quedan x, y, f, c?

Descargar ejemplo | Descargar test

Operadores

Operadores

  • Son elementos del lenguaje que se aplican a variables o literales en una expresión:
    • Asignación: = retorna el valor asignado, de forma tal de permitir asignaciones encadenadas
    • Operadores artiméticos: +, -, *, /, %
    • Operadores lógicos: &&, || !
    • Operadores relacionales: ==, !=,<, >,..

Cualquier valor distinto de 0 (cero) es VERDADERO. El 0 (cero) es FALSO

Operadores: errores

Es común usar de forma equivocada el símbolo = como comparación

int x = 3;

if ( x = 3 ) /* siempre será true */

if ( x == 3 ) /* Uso correcto */

Operadores: asignación

Se definen los operadores +=, -=, *=, /=, etc

Permiten realizar una asignación comprimida


x += 6;  /* equivale a x = x + 6  */

Operadores: pre/post-incremento

Permiten incrementar/decrementar una variable en 1

Puede ser en forma prefija o postfija:


x++;

++x;

/*  Ambos equivalen a x = x + 1 */

¿Con qué valores quedan?

int n = 5, x, y;

x = ++n;

y = n++;

Descargar ejemplo | Descargar test

Más operadores

El operador condicional if then else: _?_:_


e1 ? e2 : e3

El operador sizeof que devuelve el tamaño de un tipo o variable

int x=2;

sizeof(x);

sizeof(int);

Descargar ejemplo

Operadores para manipular bits

  • & (AND), | (OR), ^ (OR exclusivo), <<, >> (desplazamientos de bits a derecha o izquierda), ~(complemento a 1)
  • No se aplican a operandos del tipo float o double
  • No se debe confundir con los operadores lógicos && y con ||

Descargar ejemplo

Operadores: precedencia

  • () [] ->
  • ! ~ ++ -- - (tipo) * & sizeof
  • * / %
  • + -
  • << >>
  • == !=

continúa...

Operadores: precedencia

  • &
  • ^
  • |
  • &&
  • ||
  • ?
  • = += -= *= /=
  • ,

Clase 2

Temario

  • Estructuras de control
    • Sentencias compuestas
    • Condicional simple
    • Switch
    • Iteraciones

Estructuras de control

Estructuras de control

  • Gobiernan la ejecución de un programa
  • Permiten repetir código o saltear algún conjunto de sentencias.

Sentencias compuestas

Sentencias compuestas

  • También llamadas bloques
  • Suelen ser parte de otras estructuras como if, for , while, etc
  • Consiste de varias sentencias encerradas entre llaves { }
  • No terminan con ;
{
  int x = 12;
  y = x*2;
}

Descargar ejemplo

Condicional simple

Condicional simple: if

if (condicion) {
  sentencias_verdad
} else {
  sentencias_falso
}

Ejemplo

if (x == 3) {
  ...
} else {
  ...
}

Buenas prácticas

En vez de....

if (x != 0) {
...
}

if (x == 0) {
...
}

Usar...


if (x) {
...
}

if (!x) {
...
}

Condicional compuesto

Condicional compuesto: switch

switch (expresión) {
  case expresion-constante: sentencias
  case expresion-constante: sentencias
  case expresion-constante: sentencias
  default: sentencias
}

Ejemplo

...
switch (letra) {
  case 'a': case 'e': case 'i': case 'o': case 'u':
    vocMin++;
    break;
  case 'A': case 'E': case 'I': case 'O': case 'U':
    vocMay++;
    break;
  default:
    otros++;
    break;
}
...

Descargar ejemplo

El ejemplo lee las palabras como argumentos al programa

Iteraciones

Iteraciones: while

while (expresión)
  sentencias

Ejemplo

int suma=0,n=10;
while (n--) {
  suma += n;
  ....
}

/* Similar a 
while (n) {
  n = n - 1;
  suma = suma + n;
  ...
*/

Iteraciones: do while

do
  sentencias
while (expresión)

Ejemplo

int sumo = 0, n = 10;
do
  sumo += n
while (n--);

Descargar ejemplo

El ejemplo retorna el valor calculado. Este valor puede verse con echo $?

Iteraciones: for

El for siguiente...

for (expresión1; expresión2; expresión3)
  sentencias;

Equivale a...


expresión1;
while (expresión2) {
  sentencias;
  expresión3
}

Iteraciones: for

Versión 1

int suma=0,n;
for( n=1; n <=10;n++)
  suma+=n;

Versión 2

int suma=0, n=5;
for( ; n <=10;suma+=n, n++);

Versión 3

int suma=0, n=1;
for( ; n <=10; ) {
  suma+=n;
  n++;
}

Clase 3

Temario

  • Estructura de un programa
  • Funciones y prototipos
  • Entrada / Salida

Estructura de un programa

Estructura de un programa

  • Conjunto de funciones disjuntas
  • No hay anidamiento de funciones
  • Función main()
  • Prototipos
  • Compilación separada
    • Archivos .c y .h

Funciones y prototipos

Funciones en C


tipo_retornado nombre_función(lista de_argumentos)
{
  declaraciones_y_sentencias;
  [return expresion;]
}

  • Si se omite el tipo_retornado se asume int
  • Si la lista de_argumentos es vacía o la función retorna vacío usar la palabra clave void
  • Argumentos sólo por valor

Ejemplo


int sumo_n (int n)
{
  int suma=0;

  for (; n>0; suma += n, n--);

  return suma;
}

Función main


#include <stdio.h>

int main()
{
  printf (“Hola Mundo”);

  return 0;
}

Prototipo de una función

  • Las funciones deben ser declaradas antes de ser utilizadas
  • Permiten que el compilador lleve a cabo una fuerte comprobación de tipos
  • Valida también la cantidad de parámetros
  • Se incluyen antes de las funciones de nuestro programa, al principio del archivo
  • Se usa la palabra clave void para indicar el no retorno de valor por una función y una lista de argumentos vacía
  • No es necesario dar nombre a las variables en la lista de argumentos
  • Colabora con la modularización y construcción de librerías.

tipo nombre_funcion (tipo_p1, tipo_p2, tipo_p3)

Entrada/Salida

Funciones de Entrada/Salida

  • C no prove palabras claves para realizar E/S
  • Librería stdio.h de la biblioteca estándard
  • Existen funciones para la E/S por consola y por archivo, aunque técnicamente son similares conceptualmente son diferentes.

Entrada / Salida estándar

  • Archivos estándar
    • STDIN: Standard Input
    • STDOUT: Standard Output
    • STDERR: Standard Error
  • También se pueden redirigir
    • STDIN: se puede redirigir con <
    • STDOUT: se puede redirigir con >
    • STDERR: se puede redirigir con 2>

Entrada y Salida de Caracteres


int getchar(void);

int putchar(int c);

  • getchar() devuelve un entero pero se puede asignar a un char. Esto es porque devuelve EOF cuando se produce un error o alcanza el fin de archivo
  • EOF es -1 que no es un valor válido para un char.

  • putchar(int) envía a la salida estándar el caracter escrito. Envía el byte menos significativo.

Analizar por qué EOF no es valido para un char

Buffer intermedio

  • La función getchar() toma los datos de un buffer intermedio, temportal, que se carga con lo que ingresa el usuario desde teclado (STDIN)
  • Lo maneja internamente el Sistema Operativo

Buffer intermedio

Analizar stty -icanon

Buffer intermedio: ejemplo

Si un programa presenta en pantalla la siguiente pregunta:


 ¿Desea ver el informe de nuevo (presione S/N)? _

  • Asumiendo se presiona S, hasta que no se presione ENTER no pasan los datos ingresados del buffer al programa
  • Al programa llegan ambos caracteres: la tecla S y el ENTER

Buffer intermedio

  • Este buffer puede ser molesto en entornos interactivos
  • getchar() lee de a un carácter y si ingresa más de uno quedan esperando en la cola de entrada
  • Este buffer es configurable manipulando atributos de la terminal
    • Más información en man termios
  • Probar stty -icanon && cat
    • Se vuelve con stty icanon

Ejemplo de getchar y putchar: cat

#include <stdio.h>  

int main(void) 
{
  int c;

  printf("\nIngrese un caracter "); 
  c = getchar();

  while (c != EOF) {
    putchar(c);
    c=getchar();
  }

  return 0;
}

  • Notar que
    • int c se debe a que EOF es -1. Si fuese char un valor no podría representarse
    • getchar() y putchar() trabajan caracter a caracter.

Versión mejorada


#include <stdio.h>  

int main(void)
{
  int c;

  while ( (c=getchar()) !=EOF) {
    putchar(c);
  }

  return 0;
}

Descarga de ejemplos

Entrada y Salida con formato


int printf(const char *cadena_de_control, ...)

int scanf(const char *cadena_de_control, ...)

En las entradas y salidas con formato, se pueden agregar caracteres de conversión.

printf


  int printf(formato, arg1, arg2, ...)

  • Formato es un string compuesto por dos tipos de objetos:
    • Caracteres que son copiados directamente en la salida
    • Especificaciones de conversión, cada uno de los cuales causa la conversión e impresión de los siguientes argumentos sucesivo del printf
  • Debe existir el mismo número de caracteres de formato que de argumentos
  • Retorna la cantidad de caracteres escritos y -1 si se ha producido un error.

Un primer ejemplo


int dia = 6, a = 8;

float var_flotante = 2.52345;

char mes[]="abril";

printf("Estamos en la clase de C");

printf("Hoy es lunes %d de %s", dia, mes);

printf("Ejemplo var_flotante = %0.2f", var_flotante);

Caracteres de conversión de printf

Cada especificación de conversión comienza con un % y termina con un carácter de conversión.

Entre estos dos caracteres hay otros modificadores que deben especificarse en un orden determinado

A continuación detallamos estos modificadores

Modificadores de formato

  • Un # que convierte a formato alternativo
  • Un 0 para llenar el padding con ceros
  • Un signo - que especifica ajuste a izquierda;
  • Un número que especifica un ancho mínimo del campo (si es necesario se rellena con espacios en blanco para completar este valor);
  • Un punto que separa el ancho del campo de la precisión;
  • Otro número, la precisión, que indica el número de dígitos después del punto decimal (si es float) o el número mínimo de dígitos para un entero, o para strings la cantidad máxima de caracteres

Descargar ejemplo de uso de modificadores

Los caracteres de conversión

Char Tipo Se imprime como
d,i int Número decimal
o int Octal sin signo (sin el cero inicial
x,X int Hexadecimal
u int Decimal sin signo
c int Caracter
s char * Cadena de caracters
f float Número flotante

Ejemplo


#include<stdio.h>

int main(void)
{
  int i;

  printf("%8s %8s %8s\n", "^1", "^2", "^3");

  for (i=1; i<6; i++) printf("%8d %8d %8d\n", i, i*i, i*i*i);

  return 0;
}

Descargar ejemplo

scanf

Lee datos de STDIN


  int scanf(const char *cadena_ctrl, …);

  /* Usada generalmente de la siguiente forma */
  scanf(tipo, &var)

  • Donde:
    • tipo: tipo de dato a almacenar
    • Ampersand (&): indica la dirección de memoria de la variable donde se almacenará el dato. Cuando se guardan cadenas de caracteres el & se omite
    • var: variable para almacenar el dato

Retorna el número de datos a los cuales se les ha asignado un valor con éxito

Ejemplo

#include <stdio.h>

int main(){

  int x, y, res;

  printf("\nIngrese dos valores enteros separados por -,"
         "luego presione enter:");

  res = scanf("%d-%d", &x, &y);

  printf("\nSe leyeron: \n\t1\t=>\t%d\n\t2\t=>\t%d\n"
         "El resultado del scanf fue: %d\n", x, y, res);

  return 0;
}

Descargar ejemplo 05-scanf.c

Buenas prácticas

#include <stdio.h>

int main(){
  int val;
  do {
    printf("\nIngrese un número entero (-1 termina, "
           "letras se descartan): ");
    while (scanf("%d", &val) != 1) {
      fprintf(stderr, "Error. Intente nuevamente: ");
      getchar();
    }
    printf("Se leyó: %d", val);
  } while(val != -1);
  return 0;
}

Descargar ejemplo

Consideraciones de scanf

  • La cadena de control consta de 3 clases de caracteres:
    • Especificación de formato: van precedidos por % e indican el tipo de dato a leer. Tienen orden
    • Caracteres de espacio en blanco
    • Caracteres que no son espacios en blanco.
  • Los espacios, las tabulaciones y los saltos de línea se usan como separadores de campo. Dependiendo de la indicación del formato, el espacio en blanco se interpreta de diferente manera.

Consideraciones

Ejemplo Descripcion
scanf("%c%c%c", &a, &b, &c); Ingresando "x y" queda a con x, b con blanco y c con y
scanf("%s", str); Ingresando "hola mundo" queda hola

¿Cómo leemos espacion con scanf?

Descargar ejemplo

Clase 4

Temario

  • El preprocesador
    • Introducción
    • Características
    • Directivas
    • Macros
    • Concatenación

El preprocesador

Introducción

El preprocesador

Introducción

  • Es un elemento del lenguaje C muy utilizado, que permite dar una respuesta al principio de modularización y compilación separada que promueve C
  • Las instrucciones normales no afectan al preprocesador, tiene comandos especiales
  • Ventajas
    • Los programas son más fáciles de desarrollar
    • Son más fáciles de leer
    • Son más fáciles de modificar
    • Y el código de C es más transportable entre diferentes arquitecturas de máquinas

Características

  • Se ejecuta antes que la compilación.
  • Realiza modificaciones en el archivo fuente.
    • Inclusión de textos.
    • Sustituciones.
    • Eliminar partes del fuente.
  • Trabaja únicamente sobre los fuentes
  • No tiene en cuenta ningún aspecto sintáctico ni semántico del lenguaje

Funcionamiento

Directivas

  • Una directiva es una palabra que interpreta el preprocesador
  • Comienzan con el símbolo # (numeral)
  • Están situadas a principio de línea
  • Si involucran más de una línea se incluye el carácter \ (barra invertida) al final de la línea, indicando que la línea continua abajo
  • No se utiliza el carácter ; (punto y coma) al final de la línea

Directivas

#include
#define
#undef

#if 
#else 
#endif

#ifdef 
#ifndef 
#elif 

#include

  • Incluye el contenido del archivo indicado
  • Puede usarse de las siguientes formas:
#include "nombr"

#include <nombre>

Ejemplo:

#include <stdio.h>

#include "../parent_subdir/my_header.h"

¿Cómo compilamos muchos archivos?


gcc -o prog archi1.c archi2.c

Las opciones

  • -c: preprocesa y compila a código objeto
  • -o archivo: nombre del binario definitivo
  • -Wall: muestra todos las advertencias del compilador
    • Las advertencias son errores
  • -E: sólo realiza preprocesamiento

#define


#define nombre textoReemplazo

  • Sustituye todas las ocurrencias de nombre por textoReemplazo
  • Permite definir constantes simbólicas

#define MAX 100

En ejecución no ocupa un lugar en la memoria porque se reemplaza por una constante

#define

  • El alcance es desde donde se lo declara hasta el final del archivo fuente
    • Si el define se hace en un archivo incluido, entonces su alcance se propaga al archivo que usa el #include
  • Las sustituciones se hacen por elementos y no por strings encerrados entre comillas (")
  • No hay que escribir el símbolo igual (=)
  • En general, el nombre se escribe en mayúsculas para diferenciarlo de las variables.

Errores habituales


#define MAX =100

#define MIN 10;


if (x == MAX) /* if (x == =100) */

if (x == MIN) /* if (x == 10;) */

Errores habituales


#define C A+B

D = C * C /* A+B * A+B */

La forma adecuada



#define C (A+B)


#define

También permite definir macros personalizadas


#define abs(x) ( (x) > 0 ? (x) : -(x) )

z = abs(a - 3); /* z = ( (a-3) > 0 ? (a-3) : -(a-3) ); */

¿Cuál es el error de usar...?



abs(a++);


Para tener en cuenta


#define swap(x,y) {int temp=x; x=y; y=temp;}

...

if (a>b)
  swap(a,b)
else

...

¿Cuál sería un posible problema?

Otro ejemplo con #define

#define DOS 2
#define Doble(x) (DOS*(x))
#define Cuadruple(x) (Doble(Doble(x)))

....

z=Cuadruple(A);

....

Deslozado


z = (Doble(Doble(A)));
z = ((DOS*(Doble(A))));
z = ((DOS*((DOS*(A)))));
z = ((2*(DOS*(A))));
z = ((2*((2*(A)))));

¿Cómo se comporta con recursión?

  • Se efectúan los reemplazos en orden de definición y cuando llega al último reemplazo se corta el proceso
  • No continua en loop.

#define X Y
#define Y Z
#define Z X

....

A=X;

....

/* 
A = Y;
A = Z;
A = X
*/

Otro ejemplo recursivo

#define sqrt(x) ( (x) < 0 ? 0 : sqrt(x) )

sqrt(-1);

sqrt(4);

¿Qué sucede?

Macros predefinidas

  • Se definen con dos guiones bajos __
    • __FILE__ Es una cadena de caracteres que contiene el nombre del archivo fuente
    • __LINE__ El número de línea actual
    • __DATE__ fecha de compilación
    • __TIME__ hora de la compilación
    • __STDC__ 1, si compila ANSI C

La stringificación

Operadores de Macros

  • #: si x es un parámetro de una macro, #x es el parámetro actual correspondiente representado como una cadena de caracteres.

Ejemplo


#define comoString(x) #x

comoString(3)   /* "3"      */
comoString(a b) /* "a b"    */
comoString("3") /* "\"3\""  */

La concatenación

Operadores de Macros

  • ##: Se aplica cuando se procesa la macro y se reemplazan los parámetros formales por los actuales. Los dos elementos que rodean al operador se combinan

Ejemplo


a + b##c /* a +  bc */

#define concatenar(a,b) a##b

...
char prueba[] = "Hola mundo";
printf("%s", concat(pru,eba));


Macros o funciones

Si por alguna razón sucede que existe una macro y una función con el mismo nombre, resolvemos el conflicto usando paréntesis en las funciones:

#define cuadrado(x) (x)*(x)

double (cuadrado)(double x ){
  return x*x + 1;
}

int main() {
  printf("%.2f con macro\n", cuadrado(2.0));
  printf("%.2f con funcion\n", (cuadrado)(2.0));
  return 0;
}

define vs const

Asumiendo los siguientes casos


const int MAX = 100 + 20;

#define MAX 100 + 20

En el uso MAX * MAX no se obtiene el mismo resultado

undef

  • Permite dejar sin efecto un define previo

#define MAX 12

..

#undef MAX

..

#define MAX 11


Condicionales

Compilación condicional

  • El preprocesador permite incorporar o eliminar sentencias de un código fuente según la evaluación de una expresión.

  • Es posible consultar por defined

Directivas

#if ­ #else ­ #ifdef ­ #ifndef ­ #elif ­ #endif
#if defined(MAX)
...
#endif
...
#ifdef MAX
...
#endif

#if #endif


#if DEBUG
...
#endif

Descargar ejemplo

¿Cuál es el resultado?

#define ARG 0
#define GB 1
#define ESP 2
#define PAIS_ACTIVO ESP

#if PAIS_ACTIVO == ARG
char moneda[] = "pesos";
#elif PAIS_ACTIVO == GB
char moneda[] = "libras";
#else
char moneda[] = "euro";
#endif

¿Cómo se usa en los .h?

  • Para evitar inclusiones recursivas se utiliza #ifndef
#ifndef _HEADER
#define _HEADER

....

#ifndef MAX
#define MAX 100
#endif

...

#endif

Descargar ejemplo

Opciones del gcc

#include <stdio.h>

#ifdef LINUX

#define MENSAJE "Estamos en Linux"

#else

#define MENSAJE "NO estamos en Linux"

#endif

int main(void){
  printf(MENSAJE);
}

Podemos definir la macro fuera del fuente


gcc -o prog -DLINUX archi1.c

Clase 5

Temario

  • Punteros
    • Introducción
    • Operadores & y *
    • Pasaje por referencia
    • Arreglos
    • Aritmética de punteros
    • Strings
    • Asignación dinámica
    • Arreglos de punteros

Punteros

Introducción

  • Un puntero a un dato es la dirección del primer byte que forma parte del mismo.
  • Las variables de tipo puntero contienen la dirección de memoria de un dato.
  • Un puntero consta de dos partes:
    • La dirección apuntada
    • El contenido apuntado

Es importante programarlos en forma correcta y adecuada para evitar comportamientos inesperados

Operadores & y *

Para declarar un puntero:


un_tipo * ptr; /* ptr es una variable de tipo puntero a un_tipo.
                  contiene la direccion de memoria de un dato de tipo
                  un_tipo */

Ejemplos


int * var_int;        /* para punteros a enteros */

double * var_double;  /* para punteros a double */

char * var_char;      /* para punteros a char */

Operadores & y *

Para desreferenciar una variable usamos &

int n=0;  /* n es una variable de tipo int */

int * p;  /* p es una variable de tipo puntero a un int,
             que contendrá la dirección de un int */

p = &n;   /* Se asigna a p el valor de la dirección de n */

Para referenciar el contenido de un puntero usamos *

  • *p es equivalente al nombre de la variable n
  • *p = 33 es equivalente a n = 33

Ejemplo

Supongamos el siguiente extracto de código:


  int x = 5, y = 7, ptr1 = &x, ptr2 = &y;

Analizar gráficamente qué sucede con:

  • ptr1 = ptr2
  • *ptr1 = *ptr2

Pasaje por referencia

Analizando el siguiente código

void cambiar(int a, int b){
  int aux;
  aux = a;
  a = b;
  b = aux;
}

int main (){
  int x = 10, y = 20;
  cambiar(x, y);
}

Recordar que en C no exite el pasaje por referencia.
El código anterior no cambia nada

Pasaje por referencia

Si recordamos el uso del scanf, vemos un ejemplo de cómo se simula el pasaje por referencia

#include <stdio.h>

int main(){
  int x; char mensaje[30];

  scanf("%d", &x);

  scanf("%s", mensaje);

  return 0;
}

Pero.... ¿Mensaje no se precede con &?

Arreglos

  • De los arreglos sabemos que:
    • Se definen como un_tipo variable[TAM];
    • Se indexan de 0 (cero) a TAM - 1
    • La asignación de valores puede hacerse en el momento de la declaración

Ejemplos

int x[12];

int num[] = { 1, 2 };

char cadena_1[128] = "Hola Mundo";

char cadena_2[] = "Hola";

Arreglos

  • El nombre del arreglo referencia a la dirección del elemento 0 del arreglo
  • El nombre del arreglo es una constante
  • Cuando se declara un arreglo se reservan un número específico de posiciones de memoria para ese arreglo
  • Son elementos consecutivos en la memoria
  • C NO tiene control del índice

Descargar ejemplo

Arreglos como parámetro

  • Los arreglos se pasan como parámetro sin especificar la dimensión
  • Esto es porque el nombre de un arreglo representa la posición de memoria donde se inicia
  • La dimensión del arreglo debe pasarse en otro parámetro para controlar acceder al arreglo dentro de los límites del mismo

  int sum(int vec[], int dim);

Un ejercicio

  • Implementar una función que reciba un arreglo de enteros y calcule:
    • La sumatoria
    • El mínimo
    • El máximo
    • El promedio

Y devuelva todos estos valores

Descargar ejemplo

Arreglos multidimensionales

Son arreglos de arreglos

int i, j, matrix[10][20];

for(i = 0; i < 10; i++) {
  for(j = 0; j < 20;j++) {
    matrix[i][j] = 0;
  }
}

Inicialización


int matrix_2[3][4] = { { 1, 2, 3, 4 },
                       { 5, 6, 7, 8 },
                       { 9, 10, 11, 12 } };

O alternativamente, pero NO RECOMENDADO


int matrix_3[3][4] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12 };

Arreglos multidimensionales como parámetros


void sample(int matrix[][10], int row_size);

Punteros y arreglos

Existe una fuerte relación entre punteros y arreglos

El nombre de un arreglo es una constante puntero


int my_array[] = {10, 20, 30, 40, 50};

printf("my_array[0] = %d", my_array[0]); /* Imprime: my_array[0] = 10 */

printf("*my_array = %d", *my_array);     /* Imprime: *my_array = 10 */

Todo lo que se logra con indexación de arreglos, se logra con punteros

En memoria, el arreglo se almacena


int my_array = { 1, 2 }

Variable Posición Contenido
my_array 0xA000 0XA101
my_array[0] 0xA101 1
my_array[1] 0xA105 2

my_array[0] == *my_array

my_array[1] == *(my_array + 1)

Punteros y arreglos

Cualquier expresión escrita como arreglo e índice es equivalente a una expresión escrita como un puntero y un desplazamiento

int my_array[10], *ptr;

/*

  my_array[i] equivale a *(my_array +i)

  &my_array[i] equivale a (my_array +i)

  ptr[i] equivale a *(ptr + i)

*/

Aritmética de punteros

  • Si p y q apuntan a elementos del mismo arreglo, entonces es correcto utilizar los operadores ==, !=, <, >, <=, >=
  • p < q es verdadero si p apunta a un elemento que está antes en el arreglo de lo que está el que apunta q
  • Cualquier puntero se lo puede comparar con 0 (== o !=)
  • Se puede sumar o restar un entero a un puntero:
    • p + i;
    • p - i:
  • Si p y q apuntan a elementos del mismo arreglo, entonces está permitido la resta entre p y q

No es posible

  • Sumar punteros
  • Multiplicar o dividir punteros
  • Enmascarar
  • Sumar un float o double a un puntero
  • Asignar o copiar un arreglo a otro arreglo

int array_a[] = {1, 2, 3, 4},
    array_b[] = {10, 20, 30, 40};

a = b;  /* NO ES POSIBLE */

a == b  /* Devuelve true si a y b hacen referencia a la misma
         * posición de memoria. No compara elementos 
         */

Cadenas de caracteres o strings

  • El uso más habitual de los arreglos son los strings
  • Un string es un arreglo de caracteres terminada en nulo
    • Un nulo es un 0 (cero)
  • El compilador completa este 0 (cero) automáticamente.

char cadena[11];

char cadena[]="Hola"; /* Constante string que equivale
                       * a {'H', 'o', 'l', 'a', '\0' }
                       */

  • Las librerías string.h y strings.h contienen las funciones habituales para la gestión de strings
    • man string

Reinventando la rueda

Asignación dinámica de memoria

  • Mecanismo que permite alocar memoria durante la ejecución del programa
  • La memoria asignada se toma de la heap
  • Disponemos de las siguientes funciones provistas por stdlib.h

void *calloc(size_t nmemb, size_t size);

void *malloc(size_t size);

void *realloc(void *ptr, size_t size);

void free(void *ptr);

man malloc

Ejemplos


char * p = (char *) malloc(10 * sizeof(char));

int  * q = (int *) calloc(10, sizeof(int)),
     * dyn_array = NULL,
     i;

free(p);
free (q);

for(i=1; i < 1000; i++) {
  dyn_array = realloc(dyn_array, i * sizeof(int));
}

free (dyn_array);

La precedencia es muy importante

Si recordamos la tabla de precedencia de operadores, podemos ver que las primeras líneas se enumeran así

  • () [] ->
  • ! ~ ++ -- - (tipo) * & sizeof
  • * / %
  • + -
  • << >>
  • Continúa ...

La tabla va de mayor prioridad a menor prioridad

¿Qué es lo que estaría mal en el siguiente ejemplo?

void some_function(** ptr) {
  ...
  *ptr[i] = some_var;
  ...
}

Considerar la tabla de prioridades

Ejemplo arreglo dinamico v2 con error

Un error común

  • No debe retornarse un arreglo definido como variable automática
  • Puede retornarse un puntero o usar una variable static

Arreglos de punteros

Como las variables de tipo punteros son como cualquier otra, entonces podemos almacenarlas en un arreglo

Ejemplo

  • En este caso creamos un arreglo donde cada elemento es un puntero a char

char *lineas[5];    /* Arreglo de 5 punteros a char */

  • lineas[i] es un puntero a char

  • *lineas[i] es el primer carácter de la línea i-ésima

Ejemplos de arreglos de punteros


char * palabras [ ] = {"break", "continue", "do", "if", "int"};

  • palabras[0]
    • Al igual que *palabras
    • Contiene break
  • palabras[1]
    • Al igual que *(palabras +1)
    • Contiene continue
  • palabras[1][2]
    • Al igual que *(palabras [1] +2)
    • Al igual que *(*(palabras +1)+2)
    • Contiene n

Un ejemplo

#include <stdio.h>
int main()
{
  char *dias[]= { "lunes", "martes", "miercoles",
                  "jueves", "viernes", "sabado",
                  "domingo", };
  int i;
  for (i=0; i<7; i++) printf("El dia %d es %s\n",i,dias[i]);

  return 0;
}

Descargar ejemplo que lee, ordena e imprime palabras

Arreglos de punteros multidimensionales

Analizar qué cantidad de espacio se necesita en cada caso:


  int a[10][20];

  int *b[10];

Argumentos al programa principal

  • La función main puede especificar dos parámetros
  • Generalmente a estos parámetros se los llama
    • argc: de tipo entero
    • argv: de tipo char * []
  • Crear un programa y verificar el valor de argv[0]

#include <stdio.h>

int main (int argc, char *argv[]){
  int i;
  for(i=0; i<argc; i++) printf("%s\n", argv[i]);
  return argc;
}

Clase 6

Temario

  • Estructuras
  • Definición de tipos
  • Estructuras dinámicas
  • Uniones

Estructuras

Estructuras: Introducción

  • Permiten crear un tipo de datos propio.
  • Agrupan variables bajo un nombre
  • Permite mantener junta información relacionada
  • Suele llamarse tipo de dato agregado

Ejemplo

struct etiqueta {
  tipo nombre_miembro_1;
  tipo nombre_miembro_2;
  tipo nombre_miembro_3;
}

Variables e inicialización

En el siguiente caso, se define la estructura y declaran dos variables en la misma línea

Luego se definen otras variables y realiza una asignación


struct t_student {

  int file_number;
  char name[256];

} student_1, student_2;

struct t_student student_3;
struct t_student student_4 = { 1234, "Juan Perez" };

student_2 = student_4;

Representación en memoria

  • Cuando se declara una variable, el compilador dispone automáticamente de suficiente memoria para almacenar todos sus miembros
  • Cada variable estructura contiene su propia copia de miembros de la estructura.
  • Una estructura como la anteriormente definida se representa de la siguiente forma en memoria
        +---------------------------------+-----------------+
        |           256 bytes name        | 4 bytes file_no |
        +---------------------------------+-----------------+

El ejemplo es a modo ilustrativo dado que por cuestiones de alineación se utilizan estrategias diferentes. Ver ejemplo de alineación

Variables sin nombre para el tipo

Es posible definir estructuras sin nombre de tipo, especificando el nombre de la variable


struct {
  int file_number;
  char name[256];
} student_1, student_2;

Acceso a los miembros

El punto sirve para referenciar el miembro de una estructura

  student_1.file_number;
  student_2.name;

  scanf("%s",student_3.name);
  scanf("%d", &student_3.file_number);
  printf("Student name %s (%d)",
          student_3.name,
          student_3.file_number);

Estructuras anidadas


  struct t_address {
    char address[256];
    char city[256];
  };

  struct t_student {
    int file_number;
    char name[256];
    float average_note;
    struct t_address address;
  }

Arreglos de estructuras

  • Las estructuras suelen incluirse en arreglos
  • Primero se define la estructura y luego el arreglo:
  struct t_student student_list[100];
  ...
  for(i=0; i < 100; i++) {
    printf("%d - %s",
            student_list[i].file_number,
            student_list[i].name);
  }

Inicialización de estructuras


  struct t_month {

    char * name;
    unsigned int num_days;

  } month[] = {

    {"Ninguno", 0},
    {"Enero", 31},
    {"Febrero", 28},
     .....
    {"Noviembre", 30},
    {"Diciembre", 31}
  };

Estructuras y punteros

Los punteros a estructuras se utilizan como los punteros a cualquier otra variable.

struct t_student * ptr_student;

ptr_student = & student_1;

printf("%d - %s", (*ptr_student).file_number, (*ptr_student).name);

/* O equivalentemente */

printf("%d - %s", ptr_student->file_number, ptr_student->name);

Estructuras con miembros dinámicos vs estáticos

Asumiendo los siguientes tipos de datos

struct  t_static_student {
char name[256];
float average_note;
int file_number;
} student_1, student_2;

struct t_dinamyc_student {
char *name;
float average_note;
int file_number;
} d_student_1, d_student_2;

¿Qué sucede en cada caso?


strcpy(student_1.name, "Juan");
student_1.average_note = 9.5;
student_1.file_number = 100;
student_2 = student_1

d_student_1.name = malloc(sizeof(char)*256);
strcpy(d_student_1.name, "Juan");
d_student_1.average_note = 9.5;
d_student_1.file_number = 100;
d_student_2 = d_student_1;

Analice qué sucede si modificamos student2.name y d_student_2.name

Del ejemplo anterior surge algo raro

  • Sabemos que no es posible asignar dos arreglos
  • Pero entonces en el ejemplo anterior, algo sucede cuando asignamos student2 = student1
#include <stdio.h>
int main()
{
#ifdef ARR
  char a[10],b[10] = {'a','b',0};
  a = b;
  printf("b: %s\na: %s\n",b,a);
#else
  struct { char arr[10];} a,b = { {'a', 'b', 0} };
  a = b;
  printf("b: %s\na: %s\n",b.arr,a.arr);
#endif
  return 0;
}

Descargar ejemplo y compilar con -DARR

Pasaje de estructuras como argumentos

Asumiendo la siguiente estructura

struct t_example { char x;  int y;  float z;  char s[10];} sample;

Las siguientes llamadas a función son válidas:

func1(sample.x);    /* pasa el valor carácter de x */
func2(sample.y);    /* pasa el valor entero de y */
func3(&sample.z);   /* pasa la dirección de z */
func4(sample.s);    /* pasa la dirección del string s */
func5(sample.s[2]); /* pasa el valor del carácter s[2] */
func6(e1);          /* pasa la estructura completa por valor */
func7(&e1);         /* pasa la estructura completa por ref */

Definición de tipos

Typedef

  • Permite definir tipos de datos a partir de tipos de datos existentes
  • typedef no crea un nuevo tipo sino que crea un sinónimo para uno ya existente
  typedef previous_type defined_type;

Ejemplo

typedef int t_age; 

typedef struct {
          int file_number;
          char name[256];
        } t_student;

t_age age;
t_student student;

Estructuras dinámicas

Estructuras dinámicas

Son estructuras que se definen recursivamente Se las utiliza para definir listas, árboles y grafos

struct t_node {
  t_data data;
  struct t_node *node;
}

typedef struct t_node * t_list;

Implementando una lista

  • ¿Cuáles serían las funciones necesarias para manejar la lista?
    • Crear
    • Buscar un elemento
    • Determinar si está vacía
    • Insertar elemento
    • Eliminar elemento: elimina la primer ocurrencia que encuentre
    • Destruir lista
    • Operaciones para recorrer la lista

Prototipos de la lista

void    list_create(???);

short   list_find(???, t_data);

short   list_is_empty(???);

t_data  list_add(???, t_data);

t_data  list_delete(???, tdata);

void    list_destroy(???);

Prototipos de la lista

void    list_create(t_list *);

short   list_find(t_list, t_data);

short   list_is_empty(t_list);

t_data  list_add(t_list *, t_data);

t_data  list_delete(t_list *, tdata);

void    list_destroy(t_list *);

Recorriendo la lista

  • Los tipos de datos, suelen implementar funciones asociadas que permiten recorrer las estructuras sin revelar cómo fueron implementadas para simplificar su uso
  • Siguiendo este patrón, crearemos lo que llamaremos iteradores
    • Un iterador conocerá la lista que recorre
    • Podrán existir diferentes iteradores para una misma lista, cada uno podrá estar en una posición diferente de la misma lista.
    • Los iteradores conocen la forma en que se implementa la lista para poder así recorrerla

Prototipo de un iterador

El prototipo de un iterador sería:

t_list_iterator list_iterador_create(t_list );

void list_iterator_next(t_list_iterator *);

t_data list_iterator_data(t_list_iterator );

short list_iterator_end(t_list_iterator);

Ejemplo de uso

i = list_iterador_create(list); 

while(! list_iterator_end(i)) {

  t_data aux = list_iterator_data(i);
  ...
  list_iterator_next( &i);
}

Podría usarse un for en vez de un while

Implementaciones

Uniones

Uniones

Son variables que pueden tener distinto tipo en momentos diferentes de la ejecución de un programa


union {

  int x;

  float y;

  char z[256];

} my_var;

Uso de union

  • Se utilizan como las estructuras
  • La inicialización puede realizarse sólo con el primer miembro
union {
  int x;
  float y;
  char z[256];
} my_var = { 10 };

Ejemplo

struct shape {
     int type;
     double area;
     union {
       double side;
       double ratio;
       struct {
         double height;
         double width;
       } rectangle;
      } dimension;
  } my_shape;

Clase 7

Temario

  • Tipos de variables
  • El modificador extern
  • El modificador static
  • El modificador register

Tipos de variables

Alcance y tiempo de vida de variables

  • Alcance: porción del programa donde una variable puede referenciarse por su nombre. En C depende de cómo fue declarada y si se le ha aplicado algún modificador

  • Tiempo de vida: tiempo durante el cual una variable tiene reservada memoria

Variables externas

  • Las variables pueden definirse:
    • Después de la llave de apertura de un bloque
    • Antes del nombre de una función
#include <stdio.h>

int i = 0;
int main() {
  float p = 9.0;

  printf("%d %f\n", i, p)
  return 0;
}

float z = 7.5;
void print_something(){
  int j= 5;

  printf("%d %f %d\n", i, z, j)
}

Variables externas

El alcance abarca desde la definición hasta el final del archivo

 +----- int var_1;
 |      void function_1(int a ){
 |      int aux;
 |      ...
 |      }
 |
 |  +-- int var2_;
 |  |   void function_2(void ){
 |  |    ..
 |  |   }.
 |  |
 |  |  +-int var3_;
 |  |  | ...
 |  |  |
 V  V  V

¿Cómo se las puede referenciar fuera de ese alcance?

El modificador extern

Referencia una variable que no está dentro del archivo fuente o se hace referencia antes de su definición

int var_1;
void function_1(){
  extern int var_3;
  ...
}
int var_2;
void function_2(){
  ...
}
int var_3;  
  • Las variables extern cumplen que:
    • Se inicializan solo en su definición
    • Por defecto se inicializan en 0

Descargar ejemplo

Variables estáticas

Variables estáticas

  • Una variable estática interna proporciona almacenamiento privado y permanente dentro de la función
  • Una variable estática externa limita su alcance al archivo fuente donde está definida
    • Aplica también a funciones

Variables estáticas

#include <stdio.h>

int cuento() {
  static int x;
  return x++;
}

int main() {
int i;
  for(i=0 ; i<10 ; i++) printf("%d\n", cuento());
  return 0;
}
  • El valor de x se mantiene de llamada en llamada
  • Se inicializan sólo la primera vez que entra al bloque
  • Las variables estáticas se inicializan en 0 por defecto

Descargar ejemplo

Variables Register

Variables Register

  • Si es posible estas variables estarán alocadas en los registros del procesador
  • Se utilizan en variables que se van a utilizar con mucha frecuencia
  • Es una sugerencia al compilador
  • Tienen valores iniciales indefinidos
  void f(register unsigned m){
    register int i;
    ...
  }

Clase 8

Temario

  • Archivos

Archivos

Archivos

  • En C, un archivo puede ser cualquier cosa: desde un archivo en disco hasta una impresora
  • C provee una interfaz consistente e independiente del dispositivo al que se está accediendo

Se representan internamente como

  • streams (o flujo, secuencia) de datos:
    • Utiliza el tipo FILE * definido en stdio.h
    • Se definen: FILE *fp;
  • File descriptors de bajo nivel:
    • Son enteros

Ambas representaciones proveen funciones análogas

Operaciones sobre archivos

  • Para operar con un archivo es necesario
    • Abrirlo: conectando con una fuente de datos (datos en disco o periféricos)
    • Cerrarlo: al finalizar la transferencia

Operaciones con streams

Abrir un stream

  • Se inicializa o abre un stream a través de fopen
  • Retorna NULL si hay algún error

  fp = fopen(name, mode)

  • Donde mode puede ser: r, w, a, r+, w+, a+

Modos de fopen

  • r: solo lectura al inicio
  • w: trunca o crea un archivo. Se posiciona al principio
  • a: abre un archivo para añadir datos o lo crea si no existe. Se posiciona al final
  • r+: abre como lectura y escritura. Se posiciona al principio
  • w+: abre como lectura y escritura, truncando el archivo si existe o creandolo si no existe. Se posiciona al principio
  • a+: abre como lectura y escritura o lo crea si no existe. Se posiciona al final de archivo

En todos los casos puede agregarse una b al final, pero se ignora en los sistemas POSIX. En Linux no tiene sentido, pero se aconseja su uso cuando se debe intercambiar datos con sistemas no UNIX

Cerrando un stream

Los streams se cierran utilizando fclose

#include <stdio.h>
#include <errno.h>
#include <string.h>
extern int errno;

int main(int argc, char *argv[]) {
  FILE *fp;
  if (argc==2) {
    if ((fp = fopen(argv[1], "r"))){
      printf("El arch. %s pudo abrirse\n", argv[1]);
      fclose(fp);
    }
    else printf("ERROR al abrir %s:=> %s\n", argv[1], strerror(errno));
  }
  else printf("Debe enviar nombre de archivo como parametro\n");
  return 0;
}

Descargar ejemplo

Streams estándar

  • Existen los siguientes archivos que no es necesario abrir o cerrar
    • STDIN: solo de lectura
    • STDERR: solo de escritura
    • STDOUT: solo de escritura

Lecturas desde un stream

// De a bytes:
int fgetc(FILE *)

// De a lineas:
char * fgets(char *dato, int tam, FILE *fp)

// Con formato:
int fscanf(FILE *f, const char * format, ...)

// De a bloques:
size_t fread(void *dato, size_t tam, size_t cant, FILE * f)

Escrituras desde un stream

// De a bytes:
int fputc(int c, FILE *)

// De a lineas:
int fputs(const char *dato, FILE *fp)

// Con formato:
int fprintf(FILE *f, const char * format, ...)

// De a bloques:
size_t fwrite(void *dato, size_t tam, size_t cant, FILE * f)

EOF

  • Para conocer si un archivo se ha recorrido y alcanzado el fin de archivo, se utiliza la función feof(FILE *)
  • El uso de esta función no es compatible con todas las operaciones de lectura mencionadas, esto es, aunque sí se marque el EOF, esto no sucede como se espera.

Ejemplo

  i = 0;
  while (!feof(fp)) {
    fgets(buf, sizeof(buf), fp);
    printf ("Line %4d: %s", i, buf);
    i++;
  }

Descargar ejemplo | El código repite la última línea | man fgets
awk 'BEGIN {i=1}{printf("Line %4d: %s\n", i++, $0) }'

Solucionando el problema anterior

Cuando se utiliza alguna función que manipule archivos, leer claramente el man para verificar cuál es el valor de retorno. Entonces, en el caso de fgets su uso correcto sería:

  i = 0;
  while (fgets(buf, sizeof(buf), fp) != NULL) {
    printf ("Linea %4d: %s", i, buf);
    i++;
  }

Descargar ejemplo fgets y feof
Descargar ejemplo fgetc y EOF

Reusando lo aprendido con getc y putc

En las primeras clases vimos cómo implementar el comando cat, ahora podemos implementar el comando copy que copia dos archivos

  void copy(FILE *in , FILE *out) {
    int c;
    while ((c=getc(in))!=EOF) fputc(c, out);
  }

Descargar ejemplo copy

Uso de fscanf y fprintf

Estas funciones trabajan con texto formateado, por lo que trabajarán con archivos de texto y no binarios

void parse(FILE *in , FILE *out) {
  int count;
  float f1,f2,f3;
  while ((count = fscanf(in,"%f,%f,%f", &f1, &f2, &f3)) ==3)
  {
    fprintf(out, "%.2f,%.2f,%.2f,%.2f\n", f1, f2, f3, f1+f2+f3);
  }
  if (!feof(in))
  {
    printf("Linea mal formada\n");
    myclose(in,out);
    exit(EXIT_FAILURE);
  }
}

Descargar ejemplo fscanf y fprintf Probar con el ejemplo entregado

Uso de fread y fwrite

Estas funciones trabajan bajando la memoria tal cuál se almacena en ella, por esto generalmente trabajarán con archivos binarios y no de texto

  • Consideraciones
    • No todos los procesadores manejan la misma representación interna de tipos de más de un byte
    • Acordar el tipo de endian (big o little) para utilizar en el intercambio
    • Algunas soluciones:

Descargar ejemplo fread y fwrite

Uso de fseek y ftell

Estas funciones permiten posicionarse en un archivo o determinar la posición actual del cursor en un archivo respectivamente


int fseek( FILE *flujo, long desplto, int origen);
  /* Donde origen puede ser:
   *    * SEEK_SET: desde el inicio
   *    * SEEK_CUR: desde la posición actual
   *    * SEEK_END: desde el final
   * fseek(fp, 0L, SEEK_CUR)
   * fseek(fp, 0L, SEEK_SET)
   * fseek(fp, 0L, SEEK_END)
   */

long ftell( FILE *flujo);

Descargar ejemplo tail

Operaciones con file descriptors

Operando con funciones de bajo nivel

Funciones de bajo nivel

  • En vez de streams utilizan file descriptors
    • Los file descriptors son enteros
    • Las funciones de bajo nivel proveen la misma funcionalidad que las de streams pero además otras funciones más específicas que no tienen sentido en streams
    • Existe una equivalencia entre file descriptors y streams

Los archivos estandar

  • Los archivos estandar se definen en unistd.h
    • int STDIN_FILENO
      • valor cero
    • int STDOUT_FILENO
      • valor uno
    • int STDERR_FILENO
      • valor dos

Abriendo y cerrando archivos

Las funciones de apertura y cierre son:

  int open(const char * nom, int flags, mode_t mode);
  int close(int fd)

man 2 open
man 2 close

Equivalencias con STREAMS

FILE * fdopen(int fd, const char * tipo);
int fileno(FILE * f);

man 3 fdopen
man 3 fileno

Uso de read y write

ssize_t read(int fd, void * dato, size_t tam);

ssize_t write(int fd, const void * dato, size_t tam);

man 2 read
man 2 write

Clase 9

Temario

  • Punteros a funciones
  • Punteros a void
  • Argumentos variables

Punteros a funciones

Punteros a funciones

  • Es uno de los recursos que mayor flexibilidad le otorgan al lenguaje
  • Las funciones tienen una ubicación física o dirección que corresponde al segmento de código donde comienza la función
  • Es como un alias de la función
    • Una vez que un puntero apunta a una función se puede invocar a la función a través del puntero
  • En general se utilizan para parametrizarlas funciones como argumentos de otras
  • Son punteros a código y no a datos
    • La dirección de la función se obtiene con el nombre de la misma

Punteros a funciones


type (* function_name) [(args)]

  • Los punteros a funciones pueden:
    • Pasar como argumentos
    • Ser retornados por una función
    • Ser miembros de un arreglo
    • Declararse con typedef

Ejemplo

int comprobar(const char *a, const char *b,
              int (*cmp) (const char *, const char *)) {
  return cmp(a,b);
}

Descargar ejemplo completo

Typedef y punteros a funciones

typedef double (*ptr_function)(char *);

/* Donde:
 *    * double es el tipo de retorno de la función
 *    * La función recibe un puntero a char
 *    * El nombre del tipo es ptr_function
 */

ptr_function p_func = xxx;
double test = p_func("2.13");

Descargar ejemplo

Arreglos de punteros a funciones

Podemos simplemente definir arreglos de tipo punteros a función:

float (*operations[])(float, float) = { sum, minus, ... }

o usar typedef para simplificar la lectura:

typedef float (*float_operation_t) (float, float);

float_operation_t operations[] = { sum, minus, ...}

Descargar ejemplo sin typedef
Descargar ejemplo con typedef

¿Es necesario usar &/* con punteros a función?

El estandar dice:

void message (void (*func)(int), int times)
{
  int j;
  for (j=0; j<times; ++j)
    func (j);  /* (*func) (j); would be equivalent. */
}

void example (int want_foo) 
{
  void (*pf)(int) = &bar; /* The & is optional. */
  if (want_foo)
    pf = foo;
  message (pf, 5);
}

Uso de qsort

  • La librería stdlib incluye la implementación del algoritmo de ordenamiento Quick Sort
    • Ordena cualquier arreglo, siempre que se especifique la forma en que se comparan dos elementos de ese arreglo
    • Para ello la función recibe entre sus argumentos, la función de comparación
#include <stdlib.h>

void qsort(void *base, size_t nmemb, size_t size,
          int (*compar)(const void *, const void *));

Descargar ejemplo

Punteros genéricos

Pensemos cuándo usarlos...

Desarrollar un algoritmo de manejo de lista o árbol genérico. Por genérico, entendemos que el mismo algoritmo sea independiente del dato que almacene

typedef struct node {
  void * data;
  struct node * left;
  struct node * right;
} * t_node;

Punteros genéricos

  • Un puntero genérico o puntero a void es un puntero que puede apuntar a cualquier tipo de dato.
  • Para que un puntero a void pueda ser desreferenciado debe castearse antes de ser utilizado.

int main() {
  int var1 = 1;
  double var2 = 1.0;
  void* vptr ;

  vptr = &var1;
  *(int *) vptr = 2;
  vptr = &var2;
  *(float *) vptr = 4.0;
}

¿Cómo sería el arbol genérico?

En la clase 6, vimos una implementación de un árbol binario de búsqueda implementado con enteros. Descargar ejemplo

  • La implementación genérica del árbol ahora nos permite:
    • Insertar de forma ordenada tipos difrentes
    • Recorrer el árbol en (in|pre|post)order haciendo lo que necesitemos

Descargar ejemplo de árbol binario genérico

Lista de argumentos variable

Lista de argumentos variable

Analizando printf vemos que podemos usarla de cualquiera de las siguientes formas:

  printf("Hola Mundo");

  printf("Hola %s", "Mundo");

  printf("%s %s", "Hola", "Mundo");
  • Observando detalladamente, sabemos que:
    • Un parámetro de los anteriores es obligatorio: el string
    • En ese mismo argumento, se define con el formato, cuántos argumentos se recibirán luego
  • El prototipo es
  int printf(const char * format, ...);

Uso de argumentos variables

  • El soporte lo da la librería stdagr.h
  • El ejemplo más común: printf
  • Siempre hay al menos un argumento fijo
  • Encabezado general de una función con argumentos variables:

  int my_variable_args(arg1, arg2, argN, ...);

¿Qué provee stdarg?

  • Disponemos de las siguientes macros:
    • va_start
    • va_arg
    • va_end
  • Además se define el tipo va_list que permite declarar una variable que itere sobre cada argumento.
  void va_start(va_list ptr, last_arg);
  type va_arg(va_list ptr, type);
  void va_copy(va_list target, va_list source);
  void va_end(va_list ptr);

¿Cómo usar stdarg?

  • La función que desee implementar argumentos variables debe tener al menos un parámetro previo a la lista variable de argumentos, denominado last_arg
  • last_arg se lo utiliza como segundo argumento en va_start
  • va_start inicializa la lista para acceder a cualquier argumento variable
  • va_arg se utiliza luego para tomar los argumentos, siendo type el tipo del siguiente argumento
  • Una vez leídos todos los argumentos se llama a va_end para restaurar la pila

Descargar ejemplo

Variantes de printf

La familia de funciones que se ofrecen por stdio considera:

 int vprintf(const char *format, va_list ap);
 int vfprintf(FILE *stream, const char *format, va_list ap);
 int vsprintf(char *str, const char *format, va_list ap);
 int vsnprintf(char *str, size_t size, const char *format, va_list ap);

Descargar ejemplo